/**
 * 
 */
package sk.stuba.fiit.foo07.genex.generator;

import java.sql.Connection;
import java.sql.SQLException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Random;

import sk.stuba.fiit.foo07.genex.beans.Question;
import sk.stuba.fiit.foo07.genex.beans.QuestionPoints;
import sk.stuba.fiit.foo07.genex.beans.Test;
import sk.stuba.fiit.foo07.genex.dao.QuestionDao;
import sk.stuba.fiit.foo07.genex.dao.QuestionDaoDerby;
import sk.stuba.fiit.foo07.genex.exceptions.NotEnoughQuestionsException;

/**
 * @author Martin Michalek
 * 
 * This class represents a test generator which selects questions based on the
 * desired difficulty of generated test.
 */
public class TestGenerator {

    //private helpers
    private QuestionDao questionDao;
    private ArrayList<Question> possibleQuestions;
    private ArrayList<ArrayList<Question>> possibleQuestionsByDifficulty;
    private ArrayList<Question> selectedQuestions;
    private float difficultyAchieved;
    private Random rand;

    //generating parameters
    private ArrayList<Integer> questionCategoryIDs;
    private ArrayList<Integer> keywordIDs;
    private ArrayList<Integer> questionTypeIDs;
    private int difficultyDesired;
    private int questionCount;
    private int pointsSummDesired;
    private boolean containsPictures;
    private ArrayList<Integer> difficultiesCount;

    //result values
    private Test generatedTest;

    /**
     * @return the generatedTest
     */
    public Test getGeneratedTest() {
        return generatedTest;
    }

    /**
     * 
     * @param con
     *                database connection
     * @param questionCategoryIDs
     *                list of category IDs which specify the initial set of
     *                questions for test generating
     * @param keywordIDs
     *                list of keyword IDs which specify the initial set of
     *                questions for test generating
     * @param questionTypeIDs
     *                types of questions for the test
     * @param difficultyDesired
     *                test difficulty that we want to achieve
     * @param questionCount
     *                number of questions in the test
     * @param pointsSummDesired
     *                the point value of the test we want to achieve
     * @param containsPictures
     *                false if the test must not contain questions with pictures
     */
    public TestGenerator(Connection con,
            ArrayList<Integer> questionCategoryIDs,
            ArrayList<Integer> keywordIDs, ArrayList<Integer> questionTypeIDs,
            int difficultyDesired, int questionCount, int pointsSummDesired,
            boolean containsPictures) {
        super();
        this.rand = new Random();
        this.questionCategoryIDs = questionCategoryIDs;
        this.keywordIDs = keywordIDs;
        this.questionTypeIDs = questionTypeIDs;
        this.difficultyDesired = difficultyDesired;
        this.questionCount = questionCount;
        this.pointsSummDesired = pointsSummDesired;
        this.containsPictures = containsPictures;
        this.questionDao = new QuestionDaoDerby(con);
    }

    public TestGenerator(Connection con,
            ArrayList<Integer> questionCategoryIDs,
            ArrayList<Integer> keywordIDs, ArrayList<Integer> questionTypeIDs,
            int questionCount, int pointsSummDesired, boolean containsPictures,
            ArrayList<Integer> difficultiesCount) {
        super();
        this.rand = new Random();
        this.questionCategoryIDs = questionCategoryIDs;
        this.keywordIDs = keywordIDs;
        this.questionTypeIDs = questionTypeIDs;
        this.questionCount = questionCount;
        this.pointsSummDesired = pointsSummDesired;
        this.containsPictures = containsPictures;
        this.questionDao = new QuestionDaoDerby(con);
        this.difficultiesCount = difficultiesCount;
    }

    private float countProbability(float mean, float stdDeviation, float x) {
        if (difficultyDesired == 0) {
            return 1.0f;
        } else {
            return (float) ((1 / (Math.sqrt(2 * Math.PI) * stdDeviation)) * Math
                    .exp(-(1 / (2 * stdDeviation * stdDeviation))
                            * Math.pow(x - mean, 2)));
        }
    }

    private float getStdDeviation() {
        if ((difficultyDesired == 1) || (difficultyDesired == 5)) {
            return 0.8f;
        } else if ((difficultyDesired == 2) || (difficultyDesired == 4)) {
            return 1.0f;
        } else
            return 1.25f;
    }

    private int getDeterministicSelection() {
        int oneFifth = (possibleQuestions.size() / 5);
        float reminder = (possibleQuestions.size() % 5) / 2;
        if (difficultyDesired < 3) {
            return (oneFifth * (difficultyDesired - 1)) + myRandom(oneFifth);
        } else if (difficultyDesired == 3) {
            return (oneFifth * (difficultyDesired - 1) + (int) (reminder) + myRandom(oneFifth
                    + Math.round(reminder))) - 1;
        } else
            return possibleQuestions.size() - 1
                    - ((6 - difficultyDesired) * oneFifth) + myRandom(oneFifth);
    }

    private int myRandom(int i) {
        if (i == 0)
            return 0;
        else
            return rand.nextInt(i);
    }

    private void selectQuestions() {

        boolean wasSelected = false;
        float actualTestDifficulty = 3.0f;
        float stdDeviation = 0.0f;
        float randomQuestionProbability;
        int randomSellection = 0;
        int deterministicSelection = 0;
        Question toAdd = null;

        int hit = 0;

        selectedQuestions = new ArrayList<Question>(questionCount);

        for (int i = 0; i < questionCount; i++) {
            stdDeviation = getStdDeviation();
            wasSelected = false;

            for (int j = 0; j < possibleQuestions.size(); j++) {
                randomSellection = rand.nextInt(possibleQuestions.size());
                toAdd = possibleQuestions.get(randomSellection);
                if (Math.abs(toAdd.getDifficulty() - difficultyDesired) > 2) {
                    continue;
                }
                randomQuestionProbability = countProbability(difficultyDesired,
                        stdDeviation, toAdd.getDifficulty());

                if (rand.nextFloat() < randomQuestionProbability) {
                    selectedQuestions.add(i, toAdd);
                    possibleQuestions.remove(randomSellection);
                    wasSelected = true;
                    System.out.println("Selested question: "
                            + toAdd.getDifficulty() + " " + difficultyDesired);
                    break;
                }
            }

            if (!wasSelected) {
                deterministicSelection = getDeterministicSelection();
                if (deterministicSelection >= possibleQuestions.size()
                        || deterministicSelection == -1) {
                    deterministicSelection = possibleQuestions.size() - 1;
                }
                if (possibleQuestions.size() == 0) {
                    return;
                }
                toAdd = possibleQuestions.get(deterministicSelection);
                selectedQuestions.add(i, toAdd);
                possibleQuestions.remove(deterministicSelection);
                hit++;
            }
            actualTestDifficulty = (actualTestDifficulty * i + toAdd
                    .getDifficulty())
                    / (i + 1);
        }
        difficultyAchieved = actualTestDifficulty;
        System.out.println("hits: " + hit);
    }

    private float round(float f) {
        return Math.round(f * 2) / 2f;
    }

    private void countPoints() {
        ArrayList<QuestionPoints> points = new ArrayList<QuestionPoints>(
                questionCount);
        float point = 0.0f;

        if (pointsSummDesired == 0) {
            for (Question q : selectedQuestions) {
                point = q.getDifficulty();
                points.add(new QuestionPoints(q.getQuestionID(), point));
            }
        } else {
            float pointsDifficultyKoeficient = pointsSummDesired
                    / (difficultyAchieved * questionCount);

            for (Question q : selectedQuestions) {
                point = this.round(pointsDifficultyKoeficient
                        * q.getDifficulty());
                points.add(new QuestionPoints(q.getQuestionID(), point));
            }
        }

        generatedTest = new Test(null, null, null, null, null, null, points);

    }

    /**
     * Generates test based on input parameters. Results are stored in class
     * fields and can be obtained using getter methods: getGeneratedTest(),
     * getDifficultyAchieved(), getPointsSummAchieved()
     * 
     * @throws NotEnoughQuestionsException
     * @throws SQLException
     */
    public void generateTest() throws NotEnoughQuestionsException, SQLException {
        possibleQuestions = questionDao.getQuestionsByIDs(questionDao
                .getQuestionsForTestGenerator(questionCategoryIDs, keywordIDs,
                        questionTypeIDs, containsPictures, null));
        if (possibleQuestions.size() < questionCount) {
            throw new NotEnoughQuestionsException();
        }
        Collections.shuffle(possibleQuestions);
        Collections.sort(possibleQuestions);
        //System.out.println("Initial size: " + possibleQuestions.size());
        selectQuestions();
        System.out.println("Size after: " + possibleQuestions.size());
        countPoints();
        //TODO vyriesit zavyslosti otazok
    }

    public void assembleTest() throws NotEnoughQuestionsException, SQLException {
        int possibleQuestionCount = 0;
        possibleQuestionsByDifficulty = new ArrayList<ArrayList<Question>>(5);
        for (int i = 0; i < 5; i++) {
            possibleQuestions = questionDao.getQuestionsByIDs(questionDao
                    .getQuestionsForTestGenerator(questionCategoryIDs,
                            keywordIDs, questionTypeIDs, containsPictures,
                            i + 1));
            possibleQuestionsByDifficulty.add(i, possibleQuestions);

            if (possibleQuestions.size() < difficultiesCount.get(i)) {
                throw new NotEnoughQuestionsException(
                        "Not enough questions of difficulty " + (i + 1)
                                + " to generate Test.");
            }
            possibleQuestionCount += possibleQuestions.size();
        }

        if (possibleQuestionCount < questionCount) {
            throw new NotEnoughQuestionsException(
                    "Not enough questions to generate Test.");
        }

        Question toAdd = null;
        selectedQuestions = new ArrayList<Question>(questionCount);
        int randomIndex;
        int difficultyCount;
        int numberOfQuestionsSelected = 0;
        difficultyAchieved = 0;

        for (int j = 0; j < 5; j++) {
            difficultyCount = difficultiesCount.get(j);
            possibleQuestions = possibleQuestionsByDifficulty.get(j);
            for (int i = 0; i < difficultyCount; i++) {
                randomIndex = rand.nextInt(possibleQuestions.size());
                toAdd = possibleQuestions.get(randomIndex);
                selectedQuestions.add(toAdd);
                possibleQuestions.remove(randomIndex);
                difficultyAchieved = difficultyAchieved + toAdd.getDifficulty();
                numberOfQuestionsSelected++;
            }
        }
        for (int i = numberOfQuestionsSelected; i < questionCount; i++) {
            do {
                randomIndex = rand.nextInt(5);
                possibleQuestions = possibleQuestionsByDifficulty
                        .get(randomIndex);
            } while (possibleQuestions.size() <= 0);
            randomIndex = rand.nextInt(possibleQuestions.size());
            toAdd = possibleQuestions.get(randomIndex);
            selectedQuestions.add(toAdd);
            possibleQuestions.remove(randomIndex);
            difficultyAchieved = difficultyAchieved + toAdd.getDifficulty();

        }
        difficultyAchieved = difficultyAchieved / questionCount;
        countPoints();
    }
}
